Introduzione a Qiskit
In questo notebook esploreremo come programmare gate quantistici e circuiti quantistici con Qiskit e come eseguirli su simulatori e veri computer quantistici usando i pattern Qiskit. Più avanti introdurremo diversi metodi di codifica delle informazioni e termineremo con un esempio bonus di Teleportazione Quantistica.
Prima di iniziare
Segui le istruzioni di Installazione e configurazione se non lo hai ancora fatto, inclusi i passaggi per Configurare l'uso della IBM Quantum™ Platform.
Si consiglia di utilizzare l'ambiente di sviluppo Jupyter per interagire con i computer quantistici. Assicurati di installare il supporto di visualizzazione extra consigliato ('qiskit[visualization]'). Avrai anche bisogno del pacchetto matplotlib per la seconda parte di questo esempio.
Per apprendere i fondamenti del quantum computing, visita il corso Nozioni di base sull'informazione quantistica su IBM Quantum Learning
Importazioni
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime
# Import necessary modules for this notebook
import time
import qiskit
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector
from qiskit.visualization import plot_bloch_multivector, plot_state_qsphere
from qiskit_aer import AerSimulator
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_ibm_runtime import SamplerV2 as Sampler
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit.visualization import plot_histogram
print(qiskit.__version__)
2.3.1
Per eseguire i tuoi circuiti quantistici sull'hardware, devi prima configurare il tuo account. Puoi farlo nel seguente modo:
- Vai alla IBM Quantum® Platform aggiornata.
- Vai all'angolo in alto a destra (come mostrato nell'immagine sopra), crea il tuo token API e copialo in un luogo sicuro.
- Nella cella successiva, sostituisci
deleteThisAndPasteYourAPIKeyHerecon la tua chiave API. - Vai all'angolo in basso a sinistra (come mostrato nell'immagine sopra) e crea la tua istanza. Assicurati di scegliere il piano aperto.
- Dopo aver creato l'istanza, copia il codice CRN associato. Potrebbe essere necessario aggiornare la pagina per vedere l'istanza.
- Nella cella sottostante, sostituisci
deleteThisAndPasteYourCRNHerecon il tuo codice CRN.
Consulta questa guida per maggiori dettagli su come configurare il tuo account IBM Cloud®.
⚠️ Nota: Tratta la tua chiave API come una password sicura. Consulta la guida Configurazione Cloud per ulteriori informazioni sull'utilizzo della tua chiave API in ambienti sicuri e non attendibili.
#your_api_key = "deleteThisAndPasteYourAPIKeyHere"
#your_crn = "deleteThisAndPasteYourCRNHere"
QiskitRuntimeService.save_account(
channel="ibm_quantum_platform",
token=your_api_key,
instance=your_crn,
overwrite=True
)
1. Gate Quantistici e Circuiti Quantistici
I circuiti quantistici sono modelli di computazione quantistica in cui una computazione è una sequenza di gate quantistici. Diamo un'occhiata ad alcuni dei gate quantistici più comuni.
Gate X
Un Gate X equivale a una rotazione attorno all'asse X della sfera di Bloch di radianti. Mappa in e in . È l'equivalente quantistico del gate NOT dei computer classici e talvolta viene chiamato bit-flip.
# Let's apply an X-gate on a |0> qubit
qc = QuantumCircuit(1)
qc.x(0)
qc.draw(output='mpl')
# Let's see Bloch sphere visualization
sv = Statevector(qc)
plot_bloch_multivector(sv)

Gate H
Un Gate di Hadamard rappresenta una rotazione di attorno all'asse che si trova a metà tra l'asse e l'asse .
Mappa lo stato base in , il che significa che una misurazione avrà uguale probabilità di dare 1 o 0, creando una 'sovrapposizione' di stati. Questo stato è scritto anche come .
# Let's apply an H-gate on a |0> qubit
qc = QuantumCircuit(1)
qc.x(0)
qc.h(0)
qc.draw(output='mpl')
# Let's see Bloch sphere visualization
sv = Statevector(qc)
plot_bloch_multivector(sv)

Gate CX (Gate CNOT)
Il gate NOT controllato (o CNOT o CX) agisce su due qubit. Esegue l'operazione NOT (equivalente all'applicazione di un Gate X) sul secondo qubit solo quando il primo qubit è , altrimenti lo lascia invariato. Nota: Qiskit numera i bit in una stringa da destra a sinistra.
# Let's apply a CX-gate on |11>
qc = QuantumCircuit(2)
qc.x(0)
qc.x(1)
qc.cx(0,1)
qc.draw(output='mpl')
sv=Statevector(qc)
plot_state_qsphere(sv)

Crea il primo stato di Bell
# Create a Bell state circuit
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0,1)
# Draw the circuit
qc.draw("mpl")
# Plot the state using q-sphere visualization
sv = Statevector(qc)
plot_state_qsphere(sv)
# q-sphere is useful for visualizing states when Bloch sphere fails to

Crea il secondo stato di Bell
# Create a circuit with the second Bell state
qc = QuantumCircuit(2)
qc.x(0)
qc.h(0)
qc.cx(0,1)
qc.draw("mpl")
La spiegazione è che:
# Get the statevector of the circuit
sv = Statevector(qc)
# Plot the state using qsphere visualization
plot_state_qsphere(sv)

Crea lo stato GHZ a 3 qubit
# Create a circuit with 3-qubit GHZ state
qc= QuantumCircuit(3)
qc.h(0)
qc.cx(0,1)
qc.cx(0,2)
qc.draw("mpl")
# Get the statevector of the circuit
sv = Statevector(qc)
# Plot the state using qsphere visualization
plot_state_qsphere(sv)

Crea lo stato del logo Qiskit
# Create a circuit with the Qiskit logo state
qc = QuantumCircuit(4)
qc.h(0)
qc.cx(0,1)
qc.cx(0,2)
qc.cx(0,3)
qc.x(1)
# Draw the circuit
qc.draw("mpl")
# Get the statevector of the circuit
sv = Statevector(qc)
# Plot the state using qsphere visualization
plot_state_qsphere(sv)

2. Crea ed esegui un semplice programma quantistico
I quattro passaggi per scrivere un programma quantistico usando i pattern Qiskit sono:
-
Mappa il problema in un formato nativo quantistico.
-
Ottimizza i circuiti e gli operatori.
-
Esegui usando una funzione primitiva quantistica.
-
Analizza i risultati.
2.1 Mappa il problema in un formato nativo quantistico
In un programma quantistico, i circuit quantistici sono il formato nativo per rappresentare le istruzioni quantistiche, e gli operatori rappresentano le osservabili da misurare. Quando crei un Circuit, di solito crei un nuovo oggetto QuantumCircuit, poi aggiungi istruzioni in sequenza.
La seguente cella di codice crea un Circuit che produce lo stato GHZ, ovvero uno stato in cui tre Qubit sono completamente entangled tra loro.
Qiskit SDK usa la numerazione dei bit LSb 0, dove la cifra ha valore o . Per maggiori dettagli, consulta l'argomento Bit-ordering in the Qiskit SDK.
# Create a GHZ state circuit
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0,1)
qc.cx(0,2)
# Draw the circuit
qc.draw("mpl")
Consulta QuantumCircuit nella documentazione per tutte le operazioni disponibili.
Quando crei Circuit quantistici, devi anche considerare che tipo di dati vuoi ottenere dopo l'esecuzione. Qiskit offre due modi per restituire dati: puoi ottenere una distribuzione di probabilità per un insieme di Qubit che scegli di misurare, oppure puoi ottenere il valore atteso di un'osservabile. Prepara il tuo workload per misurare il tuo Circuit in uno di questi due modi con le primitive di Qiskit (spiegate in dettaglio nel Passo 3).
Questo esempio misura i valori attesi usando il sottomodulo qiskit.quantum_info, specificato tramite operatori (oggetti matematici usati per rappresentare un'azione o un processo che modifica uno stato quantistico). La seguente cella di codice crea sei operatori di Pauli a tre Qubit: ZZZ, ZZX, ZII, XXI, ZZI e III.
# Set up six different observables.
observables_labels = ["ZZZ", "ZZX", "ZII", "XXI", "ZZI", "III"]
observables = [SparsePauliOp(label) for label in observables_labels]
print(observables)
[SparsePauliOp(['ZZZ'],
coeffs=[1.+0.j]), SparsePauliOp(['ZZX'],
coeffs=[1.+0.j]), SparsePauliOp(['ZII'],
coeffs=[1.+0.j]), SparsePauliOp(['XXI'],
coeffs=[1.+0.j]), SparsePauliOp(['ZZI'],
coeffs=[1.+0.j]), SparsePauliOp(['III'],
coeffs=[1.+0.j])]
Qui, un operatore come ZZI è una notazione abbreviata per il prodotto tensoriale , che significa misurare Z sul Qubit 2 e Z sul Qubit 1 insieme, ottenendo informazioni sulla correlazione tra il Qubit 2 e il Qubit 1. I valori attesi come questo vengono solitamente scritti anche come .
Se lo stato che osserviamo è lo stato GHZ a tre Qubit, allora la misura di dovrebbe essere 1.
2.2 Ottimizza i Circuit e gli operatori
Quando si eseguono Circuit su un dispositivo, è importante ottimizzare l'insieme di istruzioni che il Circuit contiene e ridurre al minimo la profondità complessiva (approssimativamente il numero di istruzioni) del Circuit. Questo garantisce di ottenere i migliori risultati possibili riducendo gli effetti di errori e rumore. Inoltre, le istruzioni del Circuit devono essere conformi all'Instruction Set Architecture (ISA) del dispositivo Backend e devono tenere conto dei gate di base del dispositivo e della connettività dei Qubit.
Il seguente codice istanzia un dispositivo reale a cui inviare un job e trasforma il Circuit e le osservabili per adeguarle all'ISA di quel Backend. Se non hai salvato le tue credenziali in precedenza, segui le istruzioni qui per autenticarti con il tuo token API.
# Choose a real backend
service = QiskitRuntimeService(channel='ibm_quantum_platform',)
backend = service.least_busy(min_num_qubits=156)
# print backend details
print(
f"Name: {backend.name}\n"
f"Version: {backend.backend_version}\n"
f"No. of qubits: {backend.num_qubits}\n"
f"Processor type: {backend.processor_type}\n"
)
Name: ibm_marrakesh
Version: 1.0.21
No. of qubits: 156
Processor type: {'family': 'Heron', 'revision': '2'}
# option to use the AerSimulator instead of a real quantum device
seed_sim=42
backend=AerSimulator.from_backend(backend,seed_simulator=seed_sim)
Effettua il transpiling del Circuit in un Circuit ISA
# Convert to an ISA circuit and layout-mapped observables.
pm = generate_preset_pass_manager(backend=backend, optimization_level=2)
isa_circuit = pm.run(qc)
isa_circuit.draw("mpl", idle_wires=False)

mapped_observables = [
observable.apply_layout(isa_circuit.layout) for observable in observables
]
print(mapped_observables)
[SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIXIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIXIIIIIIIIIIIIIIIIIIXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j])]
2.3 Esegui usando le primitive quantistiche
I computer quantistici possono produrre risultati casuali, quindi di solito raccogli un campione degli output eseguendo il Circuit molte volte. Puoi stimare il valore dell'osservabile usando la classe Estimator. Estimator è una delle due primitive; l'altra è Sampler, che può essere usata per ottenere dati da un computer quantistico. Questi oggetti possiedono un metodo run() che esegue la selezione di Circuit, osservabili e parametri (se applicabili), usando un primitive unified bloc (PUB).
Quando esegui questo codice su hardware quantistico reale, considera l'applicazione di tecniche di mitigazione e soppressione degli errori per ridurre il rumore intrinseco del computer quantistico.
# Construct the Estimator instance.
estimator = Estimator(mode=backend)
estimator.options.resilience_level = 1
estimator.options.default_shots = 5000
Invia un job usando la primitiva Estimator.
# One pub, with one circuit to run against six different observables.
job = estimator.run([(isa_circuit, mapped_observables)])
# Use the job ID to retrieve your job data later
print(f">>> Job ID: {job.job_id()}")
>>> Job ID: 97ecd036-1767-49b0-a1dc-c71638c3c3c4
/Users/jma/miniconda3/envs/3122/lib/python3.12/site-packages/qiskit_ibm_runtime/fake_provider/local_service.py:187: UserWarning: The resilience_level option has no effect in local testing mode.
warnings.warn("The resilience_level option has no effect in local testing mode.")
Dopo che un job è stato inviato, puoi aspettare che il job venga completato nella tua istanza Python corrente, oppure usare il job_id per recuperare i dati in un secondo momento. (Consulta la sezione sul recupero dei job per i dettagli.)
Dopo che il job è completato, esamina il suo output tramite l'attributo result() del job.
# This is the result of the entire submission. You submitted one Pub,
# so this contains one inner result (and some metadata of its own).
job_result = job.result()
# This is the result from our single pub, which had six observables,
# so contains information on all six.
pub_result = job.result()[0]
Ora possiamo anche eseguire il Circuit usando la primitiva Sampler
# We include the measurements in the circuit
qc.measure_all()
sampler = Sampler(mode=backend)
qc.draw(output="mpl")

Invia un job usando la primitiva Sampler.
job_sampler = sampler.run(pm.run([qc]))
# Use the job ID to retrieve your job data later
print(f">>> Job ID: {job_sampler.job_id()}")
# Get the results
results_sampler = job_sampler.result()
>>> Job ID: a6ee4d2f-c80d-4a86-9a76-e4b1a74502e7
2.4 Analizzare i risultati
Il passaggio di analisi è tipicamente quello in cui potresti post-processare i tuoi risultati usando, ad esempio, la mitigazione degli errori di misura o l'estrapolazione a rumore zero (ZNE). Potresti inserire questi risultati in un altro workflow per ulteriori analisi o preparare un grafico dei valori e dei dati chiave. In generale, questo passaggio è specifico al tuo problema. Per questo esempio, traccia ciascuno dei valori di aspettazione misurati per il nostro Circuit.
I valori di aspettazione e le deviazioni standard per gli osservabili che hai specificato all'Estimator sono accessibili tramite gli attributi PubResult.data.evs e PubResult.data.stds del risultato del job. Per ottenere i risultati dal Sampler, usa la funzione PubResult.data.meas.get_counts(), che restituirà un dict di misurazioni sotto forma di bitstring come chiavi e conteggi come valori corrispondenti. Per ulteriori informazioni, consulta Get started with Sampler.
# Plot the result
from matplotlib import pyplot as plt
values = pub_result.data.evs
errors = pub_result.data.stds
# plotting graph
# Plotting with error bars
plt.errorbar(observables_labels, values, yerr=errors, fmt='-o', capsize=5)
plt.xlabel("Observables")
plt.ylabel("Values")
plt.title("Plot of Observables vs Values with Error Bars")
plt.grid(True)
plt.tight_layout()
plt.show()

Vediamo che gli osservabili e hanno un valore di aspettazione di 1, poiché introduce due segni meno che si annullano, e agisce come l'identità, lasciando invariato lo stato GHZ. Gli altri osservabili hanno un valore di aspettazione di 0, poiché i loro operatori introducono un numero dispari di segni meno, oppure gli operatori capovolgono un numero di qubit che rende gli stati sovrapposti ortogonali.
Ora tracciamo i risultati per il Sampler
counts_list = results_sampler[0].data.meas.get_counts()
print(counts_list)
print(f"Outcomes : {counts_list}")
display(plot_histogram(counts_list, title="GHZ state"))
{'111': 480, '000': 503, '101': 8, '100': 9, '001': 3, '011': 6, '010': 10, '110': 5}
Outcomes : {'111': 480, '000': 503, '101': 8, '100': 9, '001': 3, '011': 6, '010': 10, '110': 5}

2.5 Scalare a un grande numero di qubit
Nel quantum computing, il lavoro su scala utility è fondamentale per fare progressi nel settore. Tale lavoro richiede che i calcoli vengano eseguiti su una scala molto più grande; lavorando con Circuit che potrebbero usare oltre 100 Qubit e oltre 1000 Gate. Questo esempio fa un piccolo passo in quella direzione, scalando il problema GHZ a Qubit. Usa il workflow dei pattern di Qiskit e termina misurando il valore di aspettazione .
Step 1. Map the problem
Scrivi una funzione che restituisca un QuantumCircuit che prepara uno stato GHZ a Qubit (essenzialmente uno stato di Bell esteso), poi usa quella funzione per preparare uno stato GHZ a 10 Qubit e raccogli gli osservabili da misurare.
def get_qc_for_n_qubit_GHZ_state(n: int) -> QuantumCircuit:
qc = QuantumCircuit(n)
qc.h(0)
for i in range(n-1):
qc.cx(i, i+1)
return qc
n = 10
qc_n_GHZ = get_qc_for_n_qubit_GHZ_state(n)
qc_n_GHZ.draw("mpl")

Successivamente, mappa gli operatori di interesse. Questo esempio usa gli operatori ZZ tra i Qubit per esaminare il comportamento man mano che si allontanano. Valori di aspettazione sempre più inaccurati (corrotti) tra Qubit distanti rivelerebbero il livello di rumore presente.
# ZZII...II, ZIZI...II, ... , ZIII...IZ
operator_strings = [
"Z" + i * "I" + "Z" + "I" * (n-i-2) for i in range(n-1)
]
print(operator_strings)
print(len(operator_strings))
operators = [SparsePauliOp(operator) for operator in operator_strings]
['ZZIIIIIIII', 'ZIZIIIIIII', 'ZIIZIIIIII', 'ZIIIZIIIII', 'ZIIIIZIIII', 'ZIIIIIZIII', 'ZIIIIIIZII', 'ZIIIIIIIZI', 'ZIIIIIIIIZ']
9
Step 2. Optimize the problem for execution on quantum backend
Trasforma il Circuit e gli osservabili per corrispondere all'ISA del Backend.
# Convert to an ISA circuit and layout-mapped observables.
pm = generate_preset_pass_manager(backend=backend, optimization_level=2)
isa_circuit = pm.run(qc_n_GHZ)
isa_operators_list = [operator.apply_layout(isa_circuit.layout) for operator in operators]
Step 3. Execute on backend
Invia il job e, se lo esegui su hardware, abilita la soppressione degli errori usando una tecnica per ridurre gli errori chiamata dynamical decoupling. Il livello di resilienza specifica quanta resilienza costruire contro gli errori. Livelli più alti generano risultati più accurati, a scapito di tempi di elaborazione più lunghi. Per ulteriori spiegazioni sulle opzioni impostate nel codice seguente, consulta Configure error mitigation for Qiskit Runtime.
# Submit the circuit to Estimator
job = estimator.run([(isa_circuit, isa_operators_list)])
job_id = job.job_id()
/Users/jma/miniconda3/envs/3122/lib/python3.12/site-packages/qiskit_ibm_runtime/fake_provider/local_service.py:187: UserWarning: The resilience_level option has no effect in local testing mode.
warnings.warn("The resilience_level option has no effect in local testing mode.")
Step 4. Post-process results
Per comprendere meglio il comportamento degli stati quantistici entangled su hardware reale, analizziamo le correlazioni a coppie tra i Qubit nella base Z. In particolare, osserviamo i valori di aspettazione ⟨Z₀Zᵢ⟩, che misurano quanto fortemente il Qubit 0 è correlato con ciascun altro Qubit i. In particolare tracceremo:
Quali valori di ti aspetti di vedere nel grafico?
Opzioni:
a) Decrescenti all'aumentare di
b) Costanti in 1
c) Piccole deviazioni intorno a 1
d) Alternati tra 1 e 0 per valori dispari e pari di
data = list(range(1, len(operators) + 1)) # Distance between the Z operators
result = job.result()[0]
values = result.data.evs # Expectation value at each Z operator.
values = [
v / values[0] for v in values
] # Normalize the expectation values to evaluate how they decay with distance.
plt.plot(data, values, marker="o", label=f"{n}-qubit GHZ state")
plt.xlabel("Distance between qubits $i$")
plt.ylabel(r"$\langle Z_i Z_0 \rangle / \langle Z_1 Z_0 \rangle $")
plt.legend()
plt.show()

In questo grafico notiamo che oscilla intorno al valore 1, anche se in una simulazione ideale tutti gli dovrebbero essere 1.
Come puoi vedere, i risultati degli esperimenti a 10 Qubit sono buoni ma presentano ancora alcuni errori. Un modo per migliorare i risultati è implementare lo stato GHZ in modo più efficiente.
Di solito si implementa lo stato GHZ con una sequenza di Gate CNOT a forma di scalinata. Tuttavia, puoi implementare lo stato GHZ in modo più efficiente, riducendo la profondità dei Gate a 2 Qubit da n a n/2 o meno.
Una metrica importante per valutare quanto saranno accurati i risultati, o quanto poco rumore avrà un Circuit, è la profondità dei Gate a 2 Qubit. Questo perché i tassi di errore dei Gate a 2 Qubit (~10 volte superiori rispetto ai Gate a singolo Qubit) dominano gli errori dell'intero Circuit. Usa il seguente codice per ottenere la profondità dei Gate a 2 Qubit di un Circuit.
qc.depth(lambda x: x.operation.num_qubits == 2)
def better_ghz(n):
"fan out"
s = int(n / 2)
qc = QuantumCircuit(n)
qc.h(s)
for m in range(s, 0, -1):
qc.cx(m, m - 1)
if not (n % 2 == 0 and m == s):
qc.cx(n - m - 1, n - m)
return qc
better_ghz(n).draw("mpl")

# Check 2-qubit gate depth before transpilation
qc_better_ghz = better_ghz(n)
qc_better_ghz.depth(lambda x: x.operation.num_qubits == 2)
5
Una cosa interessante da notare qui è che siamo riusciti a ridurre la profondità quantistica del Circuit che vogliamo eseguire semplicemente essendo intelligenti e pensando a un modo diverso di programmarlo. Tuttavia, ci saranno situazioni e algoritmi in cui non potremo fare affidamento su questi trucchi ingegnosi. È qui che il Transpiler torna utile: ci aiuta a ottimizzare tutti questi aspetti in modo efficiente, così non dobbiamo preoccuparcene troppo.
3. Encoding Information
3.1 Amplitude encoding
Ora che abbiamo visto come costruire Circuit quantistici, è interessante esplorare come possiamo codificare informazioni classiche in stati quantistici. Un metodo potente è l'amplitude encoding, in cui le ampiezze di uno stato quantistico rappresentano le componenti di un vettore classico.
Consideriamo un esempio semplice. Supponiamo di voler codificare il vettore classico
in uno stato quantistico di due Qubit. L'obiettivo è preparare lo stato quantistico:
dove (o ) e il vettore è normalizzato in modo che:
Consideriamo ora l'esempio particolare:
Lo stato quantistico corrispondente è:
Questo stato può essere preparato usando una combinazione di Gate di rotazione di angoli e rispettivamente per i Qubit 0 e 1
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
import numpy as np
qc = QuantumCircuit(2)
qc.ry(np.pi / 6, 0)
qc.ry(np.pi / 4, 1)
simulator = AerSimulator()
qc.save_statevector()
result = simulator.run(qc).result()
statevector = result.get_statevector()
print("Statevector:", statevector)
qc.draw(output="mpl")
Statevector: Statevector([0.8923991 +0.j, 0.23911762+0.j, 0.36964381+0.j,
0.09904576+0.j],
dims=(2, 2))
from qiskit.quantum_info import Statevector
# Define our vector
v = np.array([0.8924, 0.3696, 0.2391, 0.0990])
v = v/np.linalg.norm(v)
# Create a statevector from the vector
state = Statevector(v)
# Initialize a quantum circuit with 2 qubits
qc = QuantumCircuit(2)
qc.initialize(state.data, [0, 1])
# Optional: simulate the state
print("Statevector:", state)
# Visualize the circuit
qc.decompose().decompose().decompose().decompose().decompose().draw("mpl")
Statevector: Statevector([0.89242154+0.j, 0.36960892+0.j, 0.23910577+0.j,
0.09900239+0.j],
dims=(2, 2))

Abbiamo quindi visto come codificare informazioni usando Gate di rotazione.
3.2 Angle encoding e Circuit parametrizzati
Un modo particolarmente interessante di codificare informazioni in un computer quantistico è progettare un Circuit quantistico che contenga alcuni angoli di rotazione o parametri che possono essere regolati per rappresentare una famiglia di funzioni . Consideriamo ad esempio il seguente Circuit quantistico parametrizzato:
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
# Define a symbolic parameter
theta = Parameter("θ")
qc = QuantumCircuit(2)
# We applied a parametrized RX gate
qc.rx(theta, 0)
qc.cx(0, 1)
qc.draw("mpl")
Matematicamente, possiamo analizzare qual è la famiglia di funzioni che possiamo rappresentare con questo Circuit:
È abbastanza chiaro che il numero di stati che possiamo rappresentare con questo Circuit quantistico è limitato, poiché non possiamo rappresentare gli stati o ad esempio. Tuttavia, la famiglia di stati che possiamo rappresentare inizia a crescere quando introduciamo più rotazioni nei posti adeguati:
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
# Define a symbolic parameter
theta1 = Parameter("θ1")
theta2 = Parameter("θ2")
qc = QuantumCircuit(2)
qc.rx(theta1, 0)
qc.rx(theta2, 1)
qc.cx(0, 1)
qc.draw("mpl")
In questo caso, gli stati quantistici che rappresenteremo sono:
\begin{align*} \text{CNOT}_{01} \, R_x^{\{1}}(\theta_2) R_x^{\{0}}(\theta_1) \ket{00} &= \text{CNOT}_{01} \, R_x^{\{1}}(\theta_2)\left( \cos(\theta_1/2)\ket{00} - i\sin(\theta_1/2)\ket{10} \right) \\ &= \text{CNOT}_{01}\left( \cos(\theta_1/2)\cos(\theta_2/2)\ket{00} - i\cos(\theta_1/2)\sin(\theta_2/2)\ket{01} \right. \\ &\quad \left. - i\sin(\theta_1/2)\cos(\theta_2/2)\ket{10} + \sin(\theta_1/2)\sin(\theta_2/2)\ket{11} \right) \\ &= \cos(\theta_1/2)\cos(\theta_2/2)\ket{00} - i\cos(\theta_1/2)\sin(\theta_2/2)\ket{01} \\ &\quad + \sin(\theta_1/2)\sin(\theta_2/2)\ket{10} - i\sin(\theta_1/2)\cos(\theta_2/2)\ket{11} \end{align*}Possiamo vedere che questo Circuit genera una famiglia più ampia di stati quantistici rispetto al precedente. In particolare, ora può produrre stati con ampiezze non nulle per o che non erano possibili con il Circuit precedente. Tuttavia, questo Circuit non è ancora un generatore universale di stati quantistici, anche se potrebbe essere sufficientemente espressivo per progettare Circuit con una certa flessibilità nella rappresentazione di determinate funzioni. In generale, più parametri indipendenti (angoli) introduciamo, più espressività ha il Circuit per approssimare stati quantistici arbitrari.
Ansatzes e Circuit library
Questo tipo di Circuit quantistico parametrizzato può essere usato per costruire Ansatzes, stati quantistici di prova che mirano ad approssimare la soluzione di un problema. Questi Ansatzes sono un componente centrale degli Algoritmi Quantistici Variazionali, una classe di algoritmi ibridi quantistico-classici che usano un computer quantistico per valutare una funzione di costo e un ottimizzatore classico per minimizzarla. Entreremo nei dettagli di questi argomenti in un'Unità successiva, ma per ora introdurremo come costruire un semplice ansatz usando la Circuit library in Qiskit.
from qiskit.circuit.library import efficient_su2
SU2_ansatz = efficient_su2(4, su2_gates=["rx", "y"], entanglement="linear", reps=1)
SU2_ansatz.decompose().draw(output="mpl")

Abbiamo visto come costruire un semplice Ansatz usando la funzione efficient_su2 della qiskit.circuit.library che sarà in grado di generare un'ampia gamma di stati quantistici regolando i suoi parametri .
Conclusione
In questo notebook, hai imparato come costruire Circuit quantistici, dalla costruzione di Gate quantistici alla definizione e misurazione di osservabili, e come eseguire questi Circuit in modo efficiente sia su simulatori che su hardware quantistico reale. Hai anche visto l'importanza di una progettazione attenta del Circuit per minimizzare gli errori quando si lavora con dispositivi quantistici reali, nonché strategie per scalare i Circuit a un numero maggiore di Qubit, in particolare attraverso l'esempio dello stato GHZ. Inoltre, hai esplorato diverse tecniche per codificare informazioni classiche in stati quantistici, tra cui l'amplitude encoding e l'angle encoding. Con tutto ciò, sei completamente equipaggiato per passare alla sessione successiva e iniziare a lavorare con gli algoritmi quantistici.
Installare Qiskit Code Assistant in VSCode
Clicca sul link e segui le istruzioni.
Bonus: Teletrasporto Quantistico
Quando senti il termine teletrasporto quantistico, potresti immaginare una tecnologia fantascientifica futuristica che disintegra un oggetto in un posto e lo fa ricomparire da qualche altra parte lontana. Ma il teletrasporto quantistico non è niente del genere. In realtà, ciò che viene teletrasportato non è la materia, è l'informazione.
Il teletrasporto quantistico consente il trasferimento dello stato quantistico di un Qubit da una posizione a un'altra. Sebbene questo trasferimento sembri istantaneo, non viola le leggi della fisica. Com'è possibile? Approfondiamo!
Il teletrasporto quantistico è un protocollo che consente a un mittente (Alice) di trasmettere lo stato di un Qubit q a un destinatario (Bob) usando due risorse fondamentali: una coppia entangled condivisa di Qubit a e b e due bit di comunicazione classica c0 e c1.
Essenzialmente ciò di cui il protocollo ha bisogno è:
q: il Qubit di Alice, inizialmente nello stato che vogliamo teletrasportare.a: la metà di Alice di una coppia entangled condivisa.b: la metà di Bob della coppia entangled condivisa.c0,c1: bit classici per memorizzare i risultati delle misurazioni di Alice.
E come funziona? Il flusso di lavoro è il seguente
- Preparare lo stato di Alice su
q. Creeremo uno stato specifico come per la verifica. - Creare l'entanglement: generare una coppia di Bell tra
aeb. - Operazioni di Alice: Alice esegue una "misurazione di Bell" sui suoi due Qubit (
qea) e memorizza i risultati classici inc0ec1. - Comunicazione classica: Alice invia i suoi due bit classici (
c0,c1) a Bob. - Correzioni di Bob: Bob applica Gate quantistici specifici (X e/o Z) al suo Qubit (
b), condizionati dai valori dic0ec1ricevuti.
Se tutto viene fatto correttamente, il Qubit b di Bob finirà nello stato , lo stato originale del q di Alice!
Per una spiegazione e un'esplorazione più approfondite del teletrasporto quantistico, inclusa la spiegazione matematica del perché questo protocollo funziona, puoi fare riferimento alle risorse di IBM Quantum Learning: Quantum Teleportation. Questo fa parte del corso Basics of Quantum Information.
import matplotlib.pyplot as plt
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit_aer import AerSimulator
from qiskit.visualization import plot_histogram, plot_bloch_multivector
# Define individual quantum registers for each qubit
q = QuantumRegister(1, name='q') # message qubit
a = QuantumRegister(1, name='a') # Alice's entangled qubit
b = QuantumRegister(1, name='b') # Bob's entangled qubit
# Classical register for Alice's measurements
cr_alice = ClassicalRegister(2, name='c_alice')
# Create quantum circuit
teleport_qc = QuantumCircuit(q, a, b, cr_alice, name='Teleportation')
# Step 1: Prepare message state |+⟩ on q
teleport_qc.h(q[0])
teleport_qc.barrier()
# Step 2: Create entanglement between a and b
teleport_qc.h(a[0])
teleport_qc.cx(a[0], b[0])
teleport_qc.barrier()
# Step 3: Alice's Bell measurement
teleport_qc.cx(q[0], a[0])
teleport_qc.h(q[0])
teleport_qc.barrier()
# Step 4: Alice measures q and a
teleport_qc.measure(q[0], cr_alice[0])
teleport_qc.measure(a[0], cr_alice[1])
teleport_qc.barrier()
# Step 5: Bob's conditional measurements
with teleport_qc.if_test((cr_alice[1], 1)):
teleport_qc.x(b[0])
with teleport_qc.if_test((cr_alice[0], 1)):
teleport_qc.z(b[0])
# Draw the circuit
teleport_qc.draw(output='mpl')

Dopo aver eseguito il protocollo sorge una domanda fondamentale: come verifichiamo che il teletrasporto abbia funzionato? Non possiamo 'vedere' direttamente lo stato del Qubit di Bob dopo il protocollo. Tuttavia, poiché abbiamo preparato lo stato iniziale di Alice (abbiamo scelto ), possiamo usare un tipo speciale di simulazione per verificare se il Qubit b di Bob è finito in quello stesso stato.
Useremo AerSimulator con save_statevector per verificare se il Qubit b di Bob finisce nello stato originale di Alice (). Questo simulatore calcola il vettore di stato finale quantistico
e poi lo rappresenta usando plot_bloch_multivector per visualizzare il Qubit di Bob (b) rispetto allo stato iniziale di Alice (q).
# Simulate the teleportation circuit
sv_simulator = AerSimulator(method='statevector')
teleport_qc_sv = teleport_qc.copy()
teleport_qc_sv.save_statevector()
# Execute the circuit on the statevector simulator
job_sv = sv_simulator.run(teleport_qc_sv)
result_sv = job_sv.result()
# Get the final statevector
final_statevector = result_sv.get_statevector()
print("Visualizing final qubit states:")
display(plot_bloch_multivector(final_statevector))
print("Note that Alice's qubits have collapsed to |00⟩, |01⟩, |10⟩, or |11⟩, while Bob's qubit is in the original state |+⟩.")
Visualizing final qubit states:

Note that Alice's qubits have collapsed to |00⟩, |01⟩, |10⟩, or |11⟩, while Bob's qubit is in the original state |+⟩.
Come possiamo vedere dalla visualizzazione, i primi due Qubit (appartenenti ad Alice) sono collassati a 0 o 1. Nel frattempo, il terzo Qubit (appartenente a Bob), rappresentato nella terza sfera di Bloch, punta lungo l'asse x, indicando che si trova nello stato , quindi abbiamo implementato con successo il protocollo di teletrasporto quantistico!
Riepilogo
A questo punto è opportuno fare un rapido riepilogo di ciò che abbiamo realizzato:
- Alice ha trasmesso uno stato quantistico sconosciuto a Bob.
- Nessuna particella fisica è stata trasferita.
- Lo stato originale sul Qubit di Alice viene distrutto, in accordo con il teorema di No-Cloning.
Tuttavia, il teletrasporto quantistico necessita ancora della comunicazione classica (i risultati delle misurazioni di Alice inviati a Bob), e questo spiega perché questo processo non consente il trasferimento di informazioni più veloce della luce ed è pienamente coerente con tutte le leggi note della fisica.